package astra.server.db;

import astra.client.services.HasId;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

import javax.servlet.ServletException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Kotelnikov Dmitry <a href="mailto:dimonnot@mail.ru"/>
 */
public class DatabaseWorker extends RemoteServiceServlet{
    public final Log log = LogFactory.getLog(DatabaseWorker.class);

    protected final Map<Class<?>, String> tableNames = new ConcurrentHashMap<Class<?>, String>();

    protected final Map<Class<?>, List<ModelField>> readCache = new ConcurrentHashMap<Class<?>, List<ModelField>>();

    protected final Map<Class<?>, StoreModelCache> storeCache = new ConcurrentHashMap<Class<?>, StoreModelCache>();

    protected final Map<Class<?>, String> removeCache = new ConcurrentHashMap<Class<?>, String>();

    protected JdbcTemplate jdbcTemplate;
	protected SingleConnectionDataSource dataSource;

	@Override
	public void init() throws ServletException {
		dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/astra", "root", "cruelty", false);
		jdbcTemplate = new JdbcTemplate(dataSource);
	}

	@Override
	public void destroy() {
		super.destroy();
		dataSource.destroy();
	}

    public String getTableName(Class<?> model) {
        String tableName = tableNames.get(model);
        if (tableName == null) {
            tableName = convertName(model.getSimpleName());
            tableNames.put(model, tableName);
        }
        return tableName;
    }

    public <T> ParameterizedRowMapper<T> getMapper(final Class<T> modelClass) {
        return new ParameterizedRowMapper<T>(){
            @Override
            public T mapRow(ResultSet resultSet, int i) throws SQLException {
                try {
                    List<ModelField> methods = readCache.get(modelClass);
                    if (methods == null) {
                        methods = new ArrayList<ModelField>();
                        Method allMethods[] = modelClass.getMethods();
                        for (int j = 0; j < allMethods.length; j++) {
                            String methodName = allMethods[j].getName();
                            Class params[] = allMethods[j].getParameterTypes();
                            if (
                                methodName.startsWith("set") && params.length == 1 &&
                                (params[0].isPrimitive() || params[0].equals(String.class))
                            ) {
                                methods.add(new ModelField(allMethods[j], convertName(methodName.substring(3)), params[0]));
                            }
                        }
                    }
                    T result = (T)modelClass.newInstance();
                    for (ModelField field:methods) {
                        try {
                            Object object;
                            try {
                                if (field.type.equals(double.class)) {
                                    object = resultSet.getDouble(field.name);
                                } else if (field.type.equals(long.class)) {
                                    object = resultSet.getLong(field.name);
                                } else if (field.type.equals(int.class)) {
                                    object = resultSet.getInt(field.name);
                                } else {
                                    object = resultSet.getString(field.name);
                                }
                            } catch (Exception e) {
                                continue;
                            }
                            field.method.invoke(result, object);
                        } catch (Exception e) {
                            log.info("Invoke exception",e);
                        }
                    }
                    return result;
                } catch (Exception ex) {
                    log.error("Error while reading model " + modelClass.getName(), ex);
                    throw new SQLException(ex);
                }
            }
        };
    }

    public void remove(HasId model) {
        String modelCache = removeCache.get(model.getClass());
        if (modelCache == null) {
            modelCache = "delete from " + convertName(model.getClass().getSimpleName()) + " where id = ?";
            removeCache.put(model.getClass(), modelCache);
        }
        jdbcTemplate.update(modelCache, model.getId());
    }

    /*
     * Store any model to database
     */
    public void store(HasId model) {
        StoreModelCache modelCache = storeCache.get(model.getClass());
        if (modelCache == null) {
            String tableName = convertName(model.getClass().getSimpleName());
            tableNames.put(model.getClass(), tableName);
            List<Method> methods = new ArrayList<Method>();
            Method allMethods[] = model.getClass().getMethods();
            StringBuilder insert = new StringBuilder();
            insert.append("insert into ").append(tableName).append('(');
            StringBuilder values = new StringBuilder();
            StringBuilder update = new StringBuilder();
            update.append("update ").append(tableName).append(" set ");
            for (int i = 0; i < allMethods.length; i++) {
                String name = allMethods[i].getName();
                Class returnType = allMethods[i].getReturnType();
                if (name.startsWith("get") && !"getId".equals(name) && (returnType.isPrimitive() || returnType.equals(String.class)) ) {
                    if (!methods.isEmpty()) {
                        insert.append(',');
                        values.append(',');
                        update.append(',');
                    }
                    methods.add(allMethods[i]);
                    String paramName = convertName(name.substring(3));
                    insert.append(paramName);
                    values.append('?');
                    update.append(paramName).append(" = ?");
                }
            }
            insert.append(") values (").append(values).append(")");
            update.append(" where id = ?");
            modelCache = new StoreModelCache(update.toString(), insert.toString(), methods);
            storeCache.put(model.getClass(), modelCache);
        }

        Object values[];

        if (model.getId() > 0) {
            values = new Object[modelCache.methods.size() + 1];
        } else {
            values = new Object[modelCache.methods.size()];
        }

        for (int i = 0; i < modelCache.methods.size(); i++) {
            try {
                values[i] = modelCache.methods.get(i).invoke(model);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        if (model.getId() > 0) {
            values[values.length - 1] = model.getId();
            jdbcTemplate.update(modelCache.update, values);
        } else {
            jdbcTemplate.update(modelCache.insert, values);
        }
    }

    public String convertName(String name) {
        StringBuilder result = new StringBuilder();
        char chars[] = name.toCharArray();
        result.append(Character.toLowerCase(chars[0]));
        for (int i = 1; i < chars.length; i++) {
            if (chars[i] >= 'A' && chars[i] <= 'Z') {
                result.append('_');
            }
            result.append(Character.toLowerCase(chars[i]));
        }
        return result.toString();
    }

    private class ModelField{
        public final Method method;
        public final String name;
        public final Class type;

        private ModelField(Method method, String name, Class type) {
            this.method = method;
            this.name = name;
            this.type = type;
        }
    }

    private class StoreModelCache{
        public final String update;
        public final String insert;
        public final List<Method> methods;

        private StoreModelCache(String update, String insert, List<Method> methods) {
            this.update = update;
            this.insert = insert;
            this.methods = methods;
        }
    }
}